#!/usr/bin/env python
# coding: utf-8

# # 基于BiLSTM的中文文本多任务实战
# 

# 在我之前的博客中我们介绍了文本的多分类的方法,我们还尝试了各种分类模型,比如朴素贝叶斯、逻辑回归、支持向量机和随机森林等并且都取得了非常不错的效果。今天我们使用深度学习中的LSTM（Long Short-Term Memory）长短期记忆网络，它是一种时间循环神经网络，适合于处理和预测时间序列中间隔和延迟相对较长的重要事件。
# LSTM 已经在科技领域有了多种应用。基于 LSTM 的系统可以学习翻译语言、控制机器人、图像分析、文档摘要、语音识别图像识别、手写识别、控制聊天机器人、预测疾病、点击率和股票、合成音乐等等任务。今天我们用它来实现一下文本多分类，相信会取得较好的效果。

# # 数据
# 
# 我们的数据来自于互联网，你可以在这里下载,数据中包含了10 个类别（书籍、平板、手机、水果、洗发水、热水器、蒙牛、衣服、计算机、酒店），共 6 万多条评论数据 首先查看一下我们的数据，这些数据都是来自于电商网站的用户评价数据,我们想要把不同评价数据分到不同的分类中去,且每条数据只能对应10个类中的一个类。
# 
# 数据下载地址:https://github.com/SophonPlus/ChineseNlpCorpus/blob/master/datasets/online_shopping_10_cats/intro.ipynb
# 

# In[3]:


#coding=utf-8
get_ipython().run_line_magic('matplotlib', 'inline')
import pandas as pd
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

from matplotlib.font_manager import FontProperties
chinese_font = FontProperties(fname='/usr/share/fonts/SimHei.ttf')

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense, Embedding, LSTM, SpatialDropout1D
from sklearn.model_selection import train_test_split
from keras.utils.np_utils import to_categorical
from keras.callbacks import EarlyStopping
from keras.layers import Dropout
from keras.layers import Input,LSTM,Bidirectional,Dense,Dropout,Concatenate,Embedding,GlobalMaxPool1D
from keras.models import Model
import keras.backend as K
import jieba as jb
import re


# In[4]:


df = pd.read_csv('./data/online_shopping_10_cats.csv')
df.info()
df.head()


# In[5]:


df = df[['cat','label','review']]
print("数据总量: %d ." % len(df))
df.sample(10)


# In[6]:


print("在 cat 列中总共有 %d 个空值." % df['cat'].isnull().sum())
print("在 review 列中总共有 %d 个空值." % df['review'].isnull().sum())
df[df.isnull().values==True]
df = df[pd.notnull(df['review'])]


# In[7]:


d = {'cat':df['cat'].value_counts().index, 'count': df['cat'].value_counts()}
df_cat = pd.DataFrame(data=d).reset_index(drop=True)
df_cat


# In[8]:



df_cat.plot(x='cat', y='count', kind='bar', legend=False,  figsize=(8, 5))
plt.title("类目分布", fontproperties=chinese_font)
plt.ylabel('数量', fontsize=18, fontproperties=chinese_font)
plt.xlabel('类目', fontsize=18, fontproperties=chinese_font)


# # 数据预处理
# 
#  接下来我们要将cat类转换成id，这样便于以后的分类模型的训练。

# In[7]:


df['cat_id'] = df['cat'].factorize()[0]
cat_id_df = df[['cat', 'cat_id']].drop_duplicates().sort_values('cat_id').reset_index(drop=True)
cat_to_id = dict(cat_id_df.values)
id_to_cat = dict(cat_id_df[['cat_id', 'cat']].values)
df.sample(10)


# In[8]:


cat_id_df


# 我们将cat转换成了Id(0到9),由于我们的评价内容都是中文,所以要对中文进行一些预处理工作,这包括删除文本中的标点符号,特殊符号,还要删除一些无意义的常用词(stopword),因为这些词和符号对系统分析预测文本的内容没有任何帮助,反而会增加计算的复杂度和增加系统开销,所有在使用这些文本数据之前必须要将它们清理干净。

# In[9]:


#定义删除除字母,数字，汉字以外的所有符号的函数
def remove_punctuation(line):
    line = str(line)
    if line.strip()=='':
        return ''
    rule = re.compile(u"[^a-zA-Z0-9\u4E00-\u9FA5]")
    line = rule.sub('',line)
    return line
 
def stopwordslist(filepath):  
    stopwords = [line.strip() for line in open(filepath, 'r', encoding='utf-8').readlines()]  
    return stopwords  

#加载停用词
stopwords = stopwordslist("./data/chineseStopWords.txt")


# 中文停用词包含了很多日常使用频率很高的常用词,如 吧，吗，呢，啥等一些感叹词等,这些高频常用词无法反应出文本的主要意思,所以要被过滤掉。

# In[10]:


#删除除字母,数字，汉字以外的所有符号
df['clean_review'] = df['review'].apply(remove_punctuation)
df.sample(10)


# 我们过滤掉了review中的标点符号和一些特殊符号，并生成了一个新的字段 clean_review。接下来我们要在clean_review的基础上进行分词,把每个评论内容分成由空格隔开的一个一个单独的词语。

# In[11]:


#分词，并过滤停用词
df['cut_review'] = df['clean_review'].apply(lambda x: " ".join([w for w in list(jb.cut(x)) if w not in stopwords]))


# In[12]:


df = df[['cat', 'cat_id', 'label', 'cut_review']]
df.sample(5)


# # BiLSTM建模 
# 
# 数据预处理完成以后，接下来我们要开始进行BiLSTM的建模工作：
# 
# * 我们要将cut_review数据进行向量化处理,我们要将每条cut_review转换成一个整数序列的向量
# * 设置最频繁使用的50000个词
# * 设置每条 cut_review最大的词语数为250个(超过的将会被截去,不足的将会被补0)

# In[13]:


# 设置最频繁使用的50000个词(在texts_to_matrix是会取前MAX_NB_WORDS,会取前MAX_NB_WORDS列)
MAX_NB_WORDS = 50000
# 每条cut_review最大的长度
MAX_SEQUENCE_LENGTH = 250
# 设置Embeddingceng层的维度
EMBEDDING_DIM = 100

tokenizer = Tokenizer(num_words=MAX_NB_WORDS, filters='!"#$%&()*+,-./:;<=>?@[\]^_`{|}~', lower=True)
tokenizer.fit_on_texts(df['cut_review'].values)
word_index = tokenizer.word_index
print('共有 %s 个不相同的词语.' % len(word_index))


# 训练和测试的数据集都准备好以后,接下来我们要定义一个LSTM的序列模型:  
# 
# * 模型的第一次是嵌入层(Embedding)，它使用长度为100的向量来表示每一个词语
# * SpatialDropout1D层在训练中每次更新时， 将输入单元的按比率随机设置为 0， 这有助于防止过拟合
# * LSTM层包含100个记忆单元
# * 输出层为包含10个分类的全连接层
# * 由于是多分类，所以激活函数设置为'softmax'
# * 由于是多分类, 所以损失函数为分类交叉熵categorical_crossentropy

# In[14]:


X = tokenizer.texts_to_sequences(df['cut_review'].values)
#填充X,让X的各个列的长度统一
X = pad_sequences(X, maxlen=MAX_SEQUENCE_LENGTH)

#多类标签的onehot展开
Y0 = pd.get_dummies(df['cat_id']).values
Y1 = pd.get_dummies(df['label']).values


# In[15]:


Y = np.concatenate((Y0, Y1),axis=1) 
#Y = np.array(list(map(lambda x:np.array(x), zip(Y0.tolist(), Y1.tolist()))),dtype=object)

print(Y.shape)
print(Y[:10])


# In[16]:


#拆分训练集和测试集
X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size = 0.10, random_state = 42)
print(X_train.shape,Y_train.shape)
print(X_test.shape,Y_test.shape)


# In[17]:



#定义模型
K.clear_session()
inputs = Input(shape=(MAX_SEQUENCE_LENGTH, ), name="sen_emb")
emb = Embedding(MAX_NB_WORDS, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH)(inputs)
x = SpatialDropout1D(0.2)(emb)
x = Bidirectional(LSTM(100, dropout=0.2, recurrent_dropout=0.2))(x)

###任务一，10分类的文本分类任务
out1 = Dense(10, activation='softmax', name="out1")(x)
###任务二，情感分类
out2 = Dense(2, activation='softmax', name="out2")(x)

# 把两个输出连起来
out = Concatenate(axis=1)([out1, out2])
model = Model(inputs=[inputs], outputs=[out])
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])  # binary_crossentropy categorical_crossentropy


#model = Model(inputs=[inputs], outputs=[out1, out2]) 
#model.compile(optimizer='adam', 
#              loss={'out1': 'categorical_crossentropy','out2': 'categorical_crossentropy'},
#              loss_weights={'out1':1, 'out2': 1},
#              metrics=["acc"])

model.summary()


# In[18]:


epochs = 3
batch_size = 64
history = model.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, validation_split=0.1,
                    callbacks=[EarlyStopping(monitor='loss', patience=3, min_delta=0.0001)])


# In[19]:


accr = model.evaluate(X_test,Y_test)
print('Test set\n  Loss: {:0.3f}\n  Accuracy: {:0.3f}'.format(accr[0],accr[1]))


# In[20]:


plt.title('Loss')
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='test')
plt.legend()
plt.show();


# In[21]:


plt.title('Accuracy')
plt.plot(history.history['accuracy'], label='train')
plt.plot(history.history['val_accuracy'], label='test')
plt.legend()
plt.show();


# # 模型的评估 
# 
# 接下来我们通过画混淆矩阵和求F1分数来评估我们模型的表现

# In[22]:


y_pred = model.predict(X_test)

pred_cat= y_pred[:,:10]
pred_lbl = y_pred[:,-2:]
y_pred_cat = pred_cat.argmax(axis = 1)
y_pred_label = pred_lbl.argmax(axis = 1)

Y_test_cat = Y_test[:,:10]
Y_test_label = Y_test[:,-2:]

Y_test_cat = Y_test_cat.argmax(axis = 1)
Y_test_label = Y_test_label.argmax(axis = 1)


# In[23]:


print(matplotlib.matplotlib_fname())


# In[75]:


import seaborn as sns
from sklearn.metrics import accuracy_score, confusion_matrix
#生成混淆矩阵
conf_mat = confusion_matrix(Y_test_cat, y_pred_cat)
fig, ax = plt.subplots(figsize=(10,8))
sns.set(font='SimHei', font_scale=0.8) 
sns.heatmap(conf_mat, annot=True, fmt='d',
            xticklabels=cat_id_df.cat.values, yticklabels=cat_id_df.cat.values)
plt.ylabel('实际结果',fontsize=18)
plt.xlabel('预测结果',fontsize=18)


# In[62]:



#生成混淆矩阵
conf_mat = confusion_matrix(Y_test_label, y_pred_label)
fig, ax = plt.subplots(figsize=(10,8))
sns.heatmap(conf_mat, annot=True, fmt='d',
            xticklabels=[0,1] , yticklabels= ['0','1'])
plt.ylabel('实际结果',fontsize=18)
plt.xlabel('预测结果',fontsize=18)


# In[43]:


from  sklearn.metrics import classification_report

print('accuracy %s' % accuracy_score(y_pred_cat, Y_test_cat))
print(classification_report(Y_test_cat, y_pred_cat, target_names=cat_id_df['cat'].values))


# In[48]:


print('accuracy %s' % accuracy_score(y_pred_label, Y_test_label))
print(classification_report(Y_test_label, y_pred_label, target_names=['0','1']))


# # 自定义预测
# 
# 首先我们定义一个预测函数:通过输入一段评语,来预测它的类目

# In[49]:


def predict(text):
    txt = remove_punctuation(text)
    txt = [" ".join([w for w in list(jb.cut(txt)) if w not in stopwords])]
    seq = tokenizer.texts_to_sequences(txt)
    padded = pad_sequences(seq, maxlen=MAX_SEQUENCE_LENGTH)
    pred = model.predict(padded)
    #print('pred:', pred)
    pred_cat= pred[:,:10]
    pred_lbl = pred[:,-2:]
    #cat_id= pred.argmax(axis=1)[0]
    cat_id= pred_cat.argmax(axis=1)[0]
    label_id = pred_lbl.argmax(axis=1)[0]
    cat_lbl = cat_id_df[cat_id_df.cat_id==cat_id]['cat'].values[0]
    label_txt = ['负面','正面'][label_id]
    return cat_lbl,label_txt
      


# In[50]:


predict('苹果好吃又不贵，已经买了很多次了')


# In[51]:


predict('收到产品已经半个多月了，一开始用着不太好用，慢慢的就好使了，可能是习惯问题吧，主要是屏的分辨率真的不错。')


# In[52]:


predict('可能中外文化理解差异，可能与小孩子太有代沟，不觉得怎么样，还是比较喜欢水墨画一点风格的漫画书，但愿我女儿长大一点能喜欢（22个月中）')


# In[53]:


predict('假的，不好用，头皮痒的要命。。。')


# In[54]:


predict('这是第三次点评，纯粹是为了拿积分。没什么可以多说了，服务不错。')


# In[55]:


#自定义review
predict('房间挺大，就是价格贵了点')


# In[56]:


#自定义review
predict('酸的要死，下次不买了')


# In[57]:


#自定义review
predict('用了几天就好卡，上当了，退款')


# In[58]:


#自定义review
predict('用了几次发现头好痒，感觉过敏了')


# In[59]:


predict('手写识别还可以')


# In[60]:


predict('T恤很好看，就是短了点')


# In[61]:


predict('吃起来太酸了，受不了，咬起来还行，能咬动')


# In[ ]:




